﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using sdd.subsystems.io;

// SDD# - Software Defined Devices in C# for Linux and Windows
// C# API for the Matrix Orbital GLK19264A-7T-1U LCD Display
// (c) 2014-2015 Chris Burri. MIT License applies. 
// See http://niston.wordpress.com

// Implementation follows the manual:
// http://www.matrixorbital.ca/manuals/GLK_Series/GLK19264A-7T-1U/GLK19264-7T-1U%20%28Rev2.0%29.pdf

namespace sdd.subsystems.display
{
    public class DisplayMxoGlk : sdd.subsystems.display.IDisplay
    {
    #region Private Vars

        // serial IO (COM, Serial-over-USB, I2C, ...)
        private ISerialIO _Port;
        
        // display write synchronization tokens
        private readonly object writeLock = new object(); // internal (single writes)
        private readonly object syncLock = new object();  // user (sequential writes)

        // display synchronous command lock
        private readonly object syncCmdLock = new object();

        // blinker interval
        private System.Timers.Timer Blinker = new System.Timers.Timer();
        
        // LED blinker variables
        private bool[] _LedBlinkEnable = new bool[3];
        private bool[] _LedBlinkToggle = new bool[3];
        private LedColors[] _LedColor = new LedColors[3];

    #endregion

    #region Constructor/Destructor

        public DisplayMxoGlk(ISerialIO port)
        {
            // set our port
            this.Port = port;

            // enable blinker support
            Blinker.Interval = 500;
            Blinker.Elapsed += Blinker_Elapsed;
            Blinker.Start();            
        }

        ~DisplayMxoGlk()
        {
            // tear down blinker support
            Blinker.Elapsed -= Blinker_Elapsed;
            Blinker.Stop();
        }

    #endregion

    #region Public Interface

        // a keypad event occured
        public event EventHandler<KeypadEventArgs> KeypadEvent;
        
        // the SerialIO implementation used for communication with the display
        public ISerialIO Port
        {
            set
            {
                if (_Port != null) { _Port.DataReceived -= _Port_OnDataReceived; }
                _Port = value;
                if (_Port != null) { _Port.DataReceived += _Port_OnDataReceived; }
            }
            get
            {
                return _Port;
            }
        }

        public object SyncLock
        {
            get
            {
                return syncLock;
            }
        }

        // Reset Display
        public void Reset()
        {
            if (Port != null) { lock (writeLock) { Port.Send(CMD_SOFTWARE_RESET); } }
        }

        // Clear Screen
        public void ClearScreen()
        {
            if (Port != null) { lock (writeLock) { Port.Send(CMD_CLEAR_SCREEN); } }
        }

        // Cursor Home
        public void Home()
        {
            if (Port != null) { lock (writeLock) { Port.Send(CMD_GO_HOME); } }
        }

        // Set Cursor Position
        public void SetCursorPosition(byte columnNumber, byte rowNumber)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_CURSOR_POSITION);                    
                    Port.Send(new byte[] {columnNumber, rowNumber});
                }
            }
        }

        // Set Cursor Coordinate
        public void SetCursorCoordinate(byte xPosition, byte yPosition)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_CURSOR_COORDINATE);
                    Port.Send(new byte[] { xPosition, yPosition });
                }
            }
        }

        // Write Text
        public void WriteText(string displayText)
        {
            if (Port != null) { lock (writeLock) { Port.Send(displayText); } }
        }

        // Initialize a Label
        public void InitializeLabel(byte labelID, byte leftPos, byte topPos, byte rightPos, byte bottomPos, JustificationVertical verticalJustification, JustificationHorizontal horizontalJustification, short fontId, byte backgroundColor, byte charSpace)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    byte[] cmdParams = new byte[] { (byte)labelID, (byte)leftPos, (byte)topPos, (byte)rightPos, (byte)bottomPos, (byte)verticalJustification, (byte)horizontalJustification };
                    
                    Port.Send(CMD_INITIALIZE_LABEL);
                    Port.Send(cmdParams);
                    Port.Send(BitConverter.GetBytes(fontId));
                    Port.Send(new byte[] { (byte)backgroundColor, (byte)charSpace });
                }
            }
        }
        
        // Update a Label
        public void UpdateLabel(byte labelID, string labelText)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_UPDATE_LABEL);
                    Port.Send(new byte[] { (byte)labelID });
                    if (labelText.Length != 0) { Port.Send(labelText); }
                    Port.Send(new byte[] { 0 });
                }
            }
        }

        // Create a Text Window
        public void InitializeTextWindow(byte windowId, byte left, byte top, byte right, byte bottom, short fontId, byte charSpace, byte lineSpace, byte scroll)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_INITIALIZE_TEXT_WINDOW);
                    Port.Send(new byte[] { windowId, left, top, right, bottom });
                    Port.Send(BitConverter.GetBytes(fontId));
                    Port.Send(new byte[] { charSpace, lineSpace, scroll });
                }
            }
        }

        // Set current Text Window by Id
        public void SetTextWindow(byte windowId)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_TEXT_WINDOW);
                    Port.Send(new byte[] { windowId });
                }
            }
        }

        // Clear Text Window by Id
        public void ClearTextWindow(byte windowId)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_CLEAR_TEXT_WINDOW);
                    Port.Send(new byte[] { windowId });
                }
            }
        }

        // Auto Scroll Enable/Disable
        public void SetAutoScroll(bool isEnabled)
        {
            if (Port != null)
            {
                lock (writeLock) { Port.Send((isEnabled ? CMD_AUTO_SCROLL_ON : CMD_AUTO_SCROLL_OFF));  }
            }
        }

        // Draw a Pixel
        public void DrawPixel(byte xPos, byte yPos)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_DRAW_PIXEL);
                    Port.Send(new byte[] { xPos, xPos });
                }
            }
        }

        // Draw a Line
        public void DrawLine(byte startX, byte startY, byte endX, byte endY)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_DRAW_LINE);
                    Port.Send(new byte[] { startX, startY, endX, endY });
                }
            }
        }

        // Draw a filled Rectangle
        public void DrawFilledRectangle(byte fillColor, byte leftPos, byte topPos, byte rightPos, byte bottomPos)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_DRAW_FILLED_RECTANGLE);
                    Port.Send(new byte[] { fillColor, leftPos, topPos, rightPos, bottomPos });
                }
            }
        }

        // Addon: Draw a percentage bar (based on filled rectangle)
        public void DrawBar(byte top, byte left, byte height, byte maxWidth, byte percent)
        {
            int width = GetPixelWidth(percent, maxWidth);
            int fill = maxWidth - width;
            lock (writeLock)
            {
                DrawFilledRectangle(1, left, top, (byte)(left + width), (byte)(top + height));
                DrawFilledRectangle(0, (byte)(left + width + 1), top, (byte)(left + width + fill), (byte)(top + height));
            }
        }

        public byte MeasureBar(byte top, byte left, byte height, byte maxWidth, byte percent)
        {
            lock (writeLock)
            {
                int width = GetPixelWidth(percent, maxWidth);
                return (byte)(left + width);
            }
        }

        // Scroll screen (region)
        public void ScrollScreen(byte leftPos, byte topPos, byte rightPos, byte bottomPos, byte moveX, byte moveY)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SCROLL_SCREEN);
                    Port.Send(new byte[] { leftPos, topPos, rightPos, bottomPos, moveX, moveY });
                }
            }
        }

        // Set current font by Id
        public void SetCurrentFont(short fontId)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_CURRENT_FONT);
                    Port.Send(BitConverter.GetBytes(fontId));
                }
            }
        }

        // Set font metrics for current font
        public void SetFontMetrics(byte lineMargin, byte topMargin, byte charSpace, byte lineSpace, byte scroll)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_FONT_METRICS);
                    Port.Send(new byte[] { lineMargin, topMargin, charSpace, lineSpace, scroll });
                }
            }
        }

        // Enable or Disable Box Space Mode
        public void SetBoxSpaceMode(bool clearExistingChar)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_BOX_SPACE_MODE);
                    Port.Send(new byte[] { (byte)(clearExistingChar ? 1 : 0) });
                }
            }
        }

        // Activate the piezo beeper
        public void ActivatePiezoBuzzer(short frequencyHz, short timeMilliseconds)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_ACTIVATE_PIEZO_BUZZER);
                    Port.Send(BitConverter.GetBytes(frequencyHz));
                    Port.Send(BitConverter.GetBytes(timeMilliseconds));
                }
            }
        }

        // Native method to set LED indicators
        private void SetLedIndicatorsNative(LedLocations ledLocation, LedColors ledColor)
        {
            if (Port != null)
            {
                lock (writeLock)
                {
                    Port.Send(CMD_SET_LED_INDICATOR);
                    Port.Send(new byte[] { (byte)ledLocation, (byte)ledColor });
                }
            }
        }

        // Set LED indicators with support for buffered updating
        public void SetLedIndicators(LedLocations location, LedColors color, bool forceUpdate = false)
        {
            int arrayLoc = (int)(location);
            if ((_LedColor[arrayLoc] != color) | forceUpdate)
            {
                _LedColor[arrayLoc] = color;

                // disable blinker when dark
                if (color == LedColors.Dark) { _LedBlinkEnable[arrayLoc] = false; }

                // set led
                SetLedIndicatorsNative(location, color);
            }
        }

        // Addon Method: control LED blinker behaviour
        public void SetLedBlinker(LedLocations location, bool enableBlinker)
        {
            _LedBlinkEnable[(int)(location)] = enableBlinker;
            // must test this:
            if (!enableBlinker) { SetLedIndicators(location, (LedColors)(_LedColor[(int)(location)])); }
        }

    #endregion

    #region Private Functions

        // convert percent to pixel width (relative to full bar length)
        private int GetPixelWidth(int percent, int maxBarLen)
        {
            return (int)Math.Floor((float)(maxBarLen) / (float)(100) * (float)(percent));
        }

        void Blinker_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // update led blinkers
            for (int i = 0; i < 3; i++)
            {
                if (_LedBlinkEnable[i])
                {
                    _LedBlinkToggle[i] = !_LedBlinkToggle[i];
                    lock (writeLock) { SetLedIndicatorsNative((LedLocations)i, (_LedBlinkToggle[i] ? LedColors.Dark : (LedColors)_LedColor[i])); }
                }
            }
        }

        void _Port_OnDataReceived(object sender, EventArgs e)
        {
            SerialIOComPort Port = (SerialIOComPort)sender;
            string inData = Port.Read();
            switch (inData)
            {
                case "B":
                    // up
                    RaiseKeypadEvent(KeypadKeys.Up);
                    break;
                case "H":
                    // down
                    RaiseKeypadEvent(KeypadKeys.Down);
                    break;
                case "C":
                    // right
                    RaiseKeypadEvent(KeypadKeys.Right);
                    break;
                case "D":
                    // left
                    RaiseKeypadEvent(KeypadKeys.Left);
                    break;
                case "E":
                    // middle
                    RaiseKeypadEvent(KeypadKeys.Center);
                    break;
                case "A":
                    // upper left key
                    RaiseKeypadEvent(KeypadKeys.App);
                    break;
                case "G":
                    // lower left key
                    RaiseKeypadEvent(KeypadKeys.System);
                    break;
            }
        }

        // Keypad event raise helper
        private void RaiseKeypadEvent(KeypadKeys key)
        {
            if (this.KeypadEvent != null) { this.KeypadEvent(this, new KeypadEventArgs(key)); }
        }   

    #endregion

    #region MatrixOrbital GLK Series Binary Commands

        // byte arrays for MXO GLK commands

        private byte[] CMD_SOFTWARE_RESET = new byte[] { 254, 253, 77, 79, 117, 110 };
        private byte[] CMD_CLEAR_SCREEN = new byte[] { 254, 88 };
        private byte[] CMD_GO_HOME = new byte[] { 254, 72 };
        private byte[] CMD_SET_CURSOR_POSITION = new byte[] { 254, 71 };
        private byte[] CMD_SET_CURSOR_COORDINATE = new byte[] { 254, 121 };
        private byte[] CMD_INITIALIZE_TEXT_WINDOW = new byte[] { 254, 43 };
        private byte[] CMD_SET_TEXT_WINDOW = new byte[] { 254, 42 };
        private byte[] CMD_CLEAR_TEXT_WINDOW = new byte[] { 254, 44 };
        private byte[] CMD_INITIALIZE_LABEL = new byte[] { 254, 45 };
        private byte[] CMD_UPDATE_LABEL = new byte[] { 254, 46 };
        private byte[] CMD_AUTO_SCROLL_ON = new byte[] { 254, 81 };
        private byte[] CMD_AUTO_SCROLL_OFF = new byte[] { 254, 82 };
        private byte[] CMD_DRAW_PIXEL = new byte[] { 254, 112 };
        private byte[] CMD_DRAW_LINE = new byte[] { 254, 108 };
        private byte[] CMD_CONTINUE_LINE = new byte[] { 254, 101 };
        private byte[] CMD_DRAW_RECTANGLE = new byte[] { 254, 114 };
        private byte[] CMD_DRAW_FILLED_RECTANGLE = new byte[] { 254, 120 };
        private byte[] CMD_SCROLL_SCREEN = new byte[] { 254, 89 };
        private byte[] CMD_SET_CURRENT_FONT = new byte[] { 254, 49 };
        private byte[] CMD_SET_FONT_METRICS = new byte[] { 254, 50 };
        private byte[] CMD_SET_BOX_SPACE_MODE = new byte[] { 254, 172 };
        private byte[] CMD_GENERAL_PURPOSE_OUTPUT_ON = new byte[] { 254, 87 };
        private byte[] CMD_GENERAL_PURPOSE_OUTPUT_OFF = new byte[] { 254, 86 };
        private byte[] CMD_SET_STARTUP_GPO_STATE = new byte[] { 254, 195 };
        private byte[] CMD_SET_LED_INDICATOR = new byte[] { 254, 90 };
        private byte[] CMD_ACTIVATE_PIEZO_BUZZER = new byte[] { 254, 187 };
        private byte[] CMD_SET_DEFAULT_BUZZER_BEEP = new byte[] { 254, 188 };
        private byte[] CMD_SET_KEYPAD_BUZZER_BEEP = new byte[] { 254, 182 };
        private byte[] CMD_AUTO_TRANSMIT_KEYPRESSES_ON = new byte[] { 254, 65 };
        private byte[] CMD_AUTO_TRANSMIT_KEYPRESSES_OFF = new byte[] { 254, 79 };
        private byte[] CMD_POLL_KEYPRESS = new byte[] { 254, 38 };
        private byte[] CMD_CLEAR_KEY_BUFFER = new byte[] { 254, 69 };
        private byte[] CMD_SET_DEBOUNCE_TIME = new byte[] {254, 85};
        private byte[] CMD_SET_AUTOREPEAT_MODE = new byte[] { 254, 126 };
        private byte[] CMD_AUTOREPEAT_MODE_OFF = new byte[] {254, 96};
        private byte[] CMD_KEYPAD_BACKLIGHT_OFF = new byte[] { 254, 155 };
        private byte[] CMD_SET_KEYPAD_BRIGHTNESS = new byte[] { 254, 156 };
        private byte[] CMD_BACKLIGHT_ON = new byte[] { 254, 66 };
        private byte[] CMD_BACKLIGHT_OFF = new byte[] { 254, 70 };
        private byte[] CMD_SET_BRIGHTNESS = new byte[] { 254, 153 };
        private byte[] CMD_SET_SAVE_BRIGHTNESS = new byte[] { 254, 152 };
        private byte[] CMD_SET_CONTRAST = new byte[] { 254, 80 };
        private byte[] CMD_SET_SAVE_CONTRAST = new byte[] { 254, 145 };
        private byte[] CMD_WRITE_CUSTOMER_DATA = new byte[] { 254, 52 };
        private byte[] CMD_READ_CUSTOMER_DATA = new byte[] { 254, 53 };
        private byte[] CMD_READ_VERSION_NUMBER = new byte[] { 254, 54 };
        private byte[] CMD_READ_MODULE_TYPE = new byte[] { 254, 55 };
        private byte[] CMD_READ_SCREEN = new byte[] { 254, 184 };
    #endregion


    }
}
